Async und await in Flutter erklärt
Wer traditionelle IDEs wie IntelliJ startet, kennt das: man muss erst einmal eine Weile warten, bis diese lahmen Dinger endlich bereit sind. Programmiersprachen wie Java erlauben zwar
langlaufende Operationen im Hintergrund zu starten, aber der Programmierer muss das immer explizit machen, und um ehrlich zu sein, so richtig elegant ist das nicht in den APIs verankert. Und wenn etwas nicht einfach ist, bzw. man nicht dazu gezwungen wird, macht man es auch eher nicht. Dart löst das anders. Operationen, die potenziell langsam sein können, wie File-Operationen oder Netzwerkzugriff, sind immer asynchron. Das heißt, man kann sie gar nicht explizit synchron aufrufen.
Zurück in die Future
Rufe ich in Dart eine Operation auf, die potenziell langsam sein könnte, bekomme ich nie direkt ein Objekt zurück, sondern immer ein Future, getypt mit dem eigentlich Objekttyp. Dieses Future hat zwei Stati, uncompleted und completed. Das Future führt die langlaufende Operation also aus und ist zunächst im Status uncompleted. Irgendwann ist die langlaufende Operation fertig und das Future ist completed. Dart bietet Code, der nicht auch asynchron ist, keine Möglichkeit, synchron auf das eigentliche Objekt zuzugreifen. Was eventuell hier noch wie Bevormundung der Entwicklergemeinschaft klingt, verhindert, dass man es sich als Programmierer leichtmachen kann und doch mal eben schnell auf das Objekt zugreift. „Wird schon nicht so lange laufen“-Denken wird hier gar nicht erst zugelassen.
Damit man aber noch etwas mit dem Future anfangen kann, kann man einen Callback registrieren, der von Dart aufgerufen wird, wenn das Future fertig ist. Damit haben wir in Dart das „Hollywood Design“-Prinzip für langsame Operationen: Versuchen Sie nicht, uns anzurufen, wir rufen Sie an. Sobald Dart das Future angeführt hat, wird der Clientcode aufgerufen, im neuen IT-Slang auch reaktives Programmieren genannt und zurzeit mit den reactive Extensions von Microsoft sehr populär. Klingt kompliziert? Ist es aber gar nicht: Listing 1 zeigt eine Dart-Methode, die einen langsamen Zugriff simuliert. Es wird mit drei Sekunden Verzug die Zahl 77 als Future berechnet.
Listing 1: Zugriffszahl in Dart Future
Future getUserId() { // simuliert eine langlaufende Operation return Future.delayed(Duration(seconds: 3), () => 77);
Wer immer diese Methode aufruft, bekommt sofort das Future zurück, d. h. die Berechnung des Callers kann weiterlaufen. Über den Callback kann man auf das Ergebnis reagieren, wenn das Future completed ist (Listing 2).
Listing 2: Callback ins Future
</strong></code> void main() { var userIdFuture = getUserId(); // hier registieren wir unseren Callback userIdFuture.then((userId) => print(userId)); // Hello wird vor 77 ausgegeben, 77 wird erst nach 3 Sekunden ausgegeben print('Hello'); }
Warte mal!
So weit, so gut. Manchmal kann das Registrieren von Callbacks, insbesondere dann, wenn man mehrere asynchrone Calls verschachteln will, doch recht komplex werden. Und Dart soll einfach bleiben, oder? Stimmt. Und deshalb kann man doch synchron auf das Ergebnis eines asynchronen Calls zugreifen, wenn man auch asynchron läuft. Asynchroner Code darf also synchron auf asynchrone Ergebnisse zugreifen. So darf eine Methode, die asynchron ist, einen REST-Call synchron absetzen und dann mit dem Ergebnis noch einmal einen Netzwerk-Call durchführen und das Ergebnis am Ende wieder als Future zurückgeben.
Eine Methode markiert sich in Dart mit dem async-Keyword als asynchron. Um den Wert eines Futures in einer solchen Methode synchron zu bekommen, wird await verwendet. Wahrscheinlich erklärt das ein Codebeispiel besser. Wir nutzen hier unsere alte getUserId()-Methode, die im synchronen Fall ein Future zurückgibt mit dem await, um auf das Ergebnis zu warten. Das geht wie gesagt nur, weil unsere Methode mit async markiert ist (Listing 3).
Listing 3: async/await
</strong></code> Future getUserName() async { var userId = await getUserId(); return Future.delayed(Duration(seconds: 1), () => "Username $userId"); }
Bisher war das alles Dart-Code. Damit wir dem Titel des Artikels noch gerecht werden, kommen wir nun zu Flutter.
Die Futures in Flutter
In Flutter sind alles Widgets. Wie spielt Flutter mit Futures zusammen? Man könnte natürlich einen Callback auf dem Flutter registrieren und in diesem Callback dann den State in einem StatefulWidget updaten und diesen State zum Bauen der Widget Hierarchy verwenden. Also in Pseudologik: Wenn ich noch keine Daten habe, dann zeige einen Platzhalter. Wenn die Daten kommen, baue mein UI neu mit den neuen Daten. Das würde gehen, wäre aber nicht so elegant, weil Dart ja sehr häufig Futures nutzt. Von daher hat Flutter das FutureBuilder-Widget. Dieses erlaubt es, effizient mit Futures zu arbeiten. Für das Beispiel erzeugt man eine Flutter-App wie im letzten Teil oder unter [2] beschrieben. Dann muss man in sein pubspec yaml noch http: ^0.12.0+4 als Dependency hinzufügen (Listing 4).
Listing 4: Dependency in Flutter
dependencies: flutter: sdk: flutter http: ^0.12.0+4
Dann kann man sein Datenmodell schreiben und es im Internet mit JSON füllen (Listing 5). Das Ganze ist asynchron.
Listing 5: Datenmodell schreiben
class Todo { int userId; String title; bool completed; Todo._internal({this.userId, this.completed, this.title}); factory Todo(Map<String, dynamic> map) { var userId = map['userId']; bool completed = map['completed']; var title = map['title']; return Todo._internal(userId: userId, completed: completed, title: title); } } Future<List> _getTodos() async { List todos = List(); http.Response response = await http.get("https://jsonplaceholder.typicode.com/todos"); if (response.statusCode == 200) { String body = response.body; var json = jsonDecode(body); for (Map<String, dynamic> map in json) { todos.add(Todo(map)); } } return todos; }
Dieses Future kann man dann mit dem FutureBuilder verwenden. In Listing 6 ist ein Widget zu sehen, das es nutzt. Das muss man nur noch in seiner App einbauen, wie im letzten Teil oder in [2] beschrieben.
Listing 6: FutureBuilder
import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; import 'dart:convert'; class TodoListWidget extends StatelessWidget { @override Widget build(BuildContext context) { return Container( child: FutureBuilder( builder: (context, AsyncSnapshot<List> snapshot) { if (snapshot.hasData) { return ListView.builder( padding: EdgeInsets.all(16), itemCount: snapshot.data.length, itemBuilder: (context, number) { return Container( decoration: BoxDecoration( border: Border.all(color: Colors.grey), borderRadius: BorderRadius.circular(5.0), ), child: ListTile( title: Text("${snapshot.data[number].title}"), subtitle: Text("${snapshot.data[number].completed}"), ), ); }, ); } else { return Center(child: CircularProgressIndicator()); } }, future: _getTodos(), ), ); } }
Das Ganze sieht dann so aus wie in Abbildung 1. Wir haben also eine Liste erzeugt, die wir asynchron mit geparstem JSON befüllt haben. Und das alles in ca. 70 Zeilen Code. Ganz schön beeindruckend, finde ich.
Links & Literatur
[1] Entwickler Magazin 1.20: Flutter-haft. Mobiles Entwicklen mit dem Flutter SDK. Lars Vogel, Jonas Hungershausen.
[2] https://www.vogella.com/tutorials/Flutter/article.html